package at.donalfonso.koch;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

import org.mozartspaces.capi3.AnyCoordinator;
import org.mozartspaces.capi3.CoordinationData;
import org.mozartspaces.capi3.CountNotMetException;
import org.mozartspaces.capi3.FifoCoordinator;
import org.mozartspaces.capi3.KeyCoordinator;
import org.mozartspaces.capi3.LabelCoordinator;
import org.mozartspaces.capi3.LindaCoordinator;
import org.mozartspaces.capi3.Selector;
import org.mozartspaces.core.Capi;
import org.mozartspaces.core.CapiUtil;
import org.mozartspaces.core.ContainerReference;
import org.mozartspaces.core.Entry;
import org.mozartspaces.core.MzsCoreException;
import org.mozartspaces.core.MzsTimeoutException;
import org.mozartspaces.core.TransactionReference;
import org.mozartspaces.core.MzsConstants.RequestTimeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import at.donalfonso.model.Bestellung;
import at.donalfonso.model.LieferBestellung;
import at.donalfonso.model.Pizza;
import at.donalfonso.model.Pizza.PizzaStatusEnum;
import at.donalfonso.utility.SpaceUtility;

public class Koch {
	private String name = "";
	private int filiale;
	private Boolean benchmark = false;
	
	private SpaceUtility space = null;

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	/**
	 * Koch ist ein Programm das auf zwei Space Containern operiert: Kueche_input und Kueche_output
	 * Das Programm entnimmt Pizzen aus der Queue Kueche_input, bearbeitet diese und legt die fertige Pizza 
	 * am Space im Container Kueche_output ab.
	 * Jede bearbeitete Pizza enthält eine Referenz auf die Bestellung. Der Koch aktualisiert die Bestellung
	 * nach jeder fertigen Pizza.
	 * @param args
	 */
	public static void main(String[] args) {
		Koch koch; 

		if (args.length < 3) {
			System.out.println("Usage: java Koch <ID> <Filiale> <Benchmark=1>");
			Random random = new Random();
			koch = new Koch("Ko" + random.nextInt(100), "1", "0");
		} 
		else {
			koch = new Koch(args[0], args[1], args[2]);
		}
		while(true) {
			koch.kochen();
		}
	}

	public Koch(String name, String fil, String benchmark) {
		this.name = name;
		filiale = Integer.parseInt(fil);
		getSpace();
		if (benchmark.equals("1"))
			this.benchmark = true;
		System.out.println("Koch: " + name + " (Filiale " + filiale + ")");
	}

	public void kochen() {
		
		Capi capi = getSpace().getCapi();

		try {

			ContainerReference kueche_input = getSpace().getKuecheInputContainer();
			ContainerReference best_offen = getSpace().getBestellungOffenContainer();
			ContainerReference best_zub = getSpace().getBestellungZubContainer();
			ContainerReference best_fertig = getSpace().getBestellungFertigContainer();
			
			TransactionReference tx1 = capi.createTransaction(20000, getSpace().getUri());
			ArrayList<Pizza> pizzen = null;
			Pizza pizza = null;
			
			// versuche, offene Pizza aus kueche_input zu nehmen,
			// wenn keine vorhanden ist, werden im else-Zweig offene Bestellungen in einzelne
			// Pizzen aufgelöst
			try {
				pizzen = capi.take(kueche_input, FifoCoordinator.newSelector(), 1000, tx1);
			} catch (CountNotMetException | MzsTimeoutException e) {
				logger.debug("kueche_input leer");
			}
			
			if (pizzen != null) {
				pizza = (Pizza) CapiUtil.getSingleEntry(pizzen);
				logger.debug("Koch liest Pizza aus Queue: " + pizza);
				pizza.setKoch(this.name);
				
				/* Warten auf die Fertigstellung */
				if (!benchmark)
					Thread.sleep(pizza.getType().getTime() * 1000);
				pizza.setStatus(PizzaStatusEnum.FERTIG);
				
				logger.debug("Koch: pizza fertig: "+pizza);
				
				// Die Pizza ist fertig: sie wird in den Container Kueche_Output gestellt 
				capi.write(new Entry(pizza, KeyCoordinator.newCoordinationData(pizza.getId())), getSpace().getKuecheOutputContainer(), RequestTimeout.DEFAULT, tx1);

				// Die Bestellung wird aktualisiert 
				Selector selector = KeyCoordinator.newSelector(pizza.getBestellnummer());
				ArrayList<Bestellung> bestellungen = capi.take(best_zub, selector, RequestTimeout.DEFAULT, tx1);
				if (bestellungen == null) {
					logger.error("Bestellung zu Pizza konnte nicht aus Bestellung_Zubereitung genommen werden.");
					capi.rollbackTransaction(tx1);
					return;
				}
				Bestellung bestellung = CapiUtil.getSingleEntry(bestellungen);
				
			
				if (bestellung != null) {
				// Der Bestellung mitteilen dass eine weitere Pizza fertig ist.
				// Die Methode ändert den Status auf "Erledigt" wenn alle Pizzen fertig sind.
					bestellung.pizzaFertig(pizza);
								
					if (bestellung.fertig()) {
					// Wenn die Bestellung erledigt ist, wird sie in den Container bestellungFertig geschrieben.
					
						logger.debug("Bestellung wird in BestellungFertig geschrieben: " + bestellung);
					
						CoordinationData coord;
						if (bestellung instanceof LieferBestellung)
							coord = LabelCoordinator.newCoordinationData(LieferBestellung.class.getName());
						else
							coord = LabelCoordinator.newCoordinationData(Bestellung.class.getName());
					
						capi.write(new Entry(bestellung, coord), best_fertig, RequestTimeout.DEFAULT, tx1);
					
					} 
					else {
					// Bestellung ist noch nicht fertig, sie wird wieder in den Container bestellungZub geschrieben.
						logger.debug("Bestellung wird in bestellung_zubereitung geschrieben: " + bestellung);
						capi.write(new Entry(bestellung, KeyCoordinator.newCoordinationData(bestellung.getBestellungId())), best_zub, RequestTimeout.DEFAULT, tx1);
					}
				}
				else {
					logger.warn("konnte Bestellung nicht mit fertiger Pizza aktualisieren");
					capi.rollbackTransaction(tx1);
				}
				
				capi.commitTransaction(tx1);
			}
			// pizza == 0, d.h. kueche_input ist leer, löse die nächste offene Bestellung auf
			else {
				capi.rollbackTransaction(tx1);
				
				int cnt_kueche_input = capi.test(space.getKuecheInputContainer(), AnyCoordinator.newSelector(Selector.COUNT_MAX), RequestTimeout.TRY_ONCE, null);
				if (cnt_kueche_input > 0) {
//					System.out.println("in else-Zweig, aber kueche_input ist gar nicht leer.");
					return;
				}
				
				// Auflösung einer offenen Bestellung in einzelne Pizzen
				// LieferBestellungen sind zu bevorzugen
				TransactionReference tx2 = capi.createTransaction(1000, getSpace().getUri());
				LieferBestellung template = new LieferBestellung();
				
				Selector sel = LindaCoordinator.newSelector(template);
				ArrayList<Bestellung> lb = null;
				
				try {
					lb = capi.take(best_offen, Arrays.asList(sel, FifoCoordinator.newSelector()), RequestTimeout.DEFAULT, tx2);
				} catch (CountNotMetException | MzsTimeoutException e) {
					// ok, keine offene LieferBestellung vorhanden
					logger.debug("keine Lieferbestellung gefunden");
				}
				if (lb == null) {
					try {
						lb = capi.take(best_offen, FifoCoordinator.newSelector(), 500, tx2);
					} catch (CountNotMetException | MzsTimeoutException e) {
						// ok, auch keine offenen Bestellungen vorhanden
					}
				}
				if (lb == null) {
					logger.debug("auch keine offenen Bestellungen vorhanden, suche wieder Pizzen");
					capi.rollbackTransaction(tx2);
					return;
				}
				
				Bestellung bestellung = CapiUtil.getSingleEntry(lb);
				logger.debug("Bestellung aus bestellung_offen: " + bestellung);
				for (Pizza p: bestellung.getPizzen()) {
					p.setStatus(PizzaStatusEnum.ZUBEREITUNG);
					p.setBestellnummer(bestellung.getBestellungId());
					capi.write(new Entry(p, FifoCoordinator.newCoordinationData()), kueche_input, RequestTimeout.DEFAULT, tx2);
				}
				capi.write(new Entry(bestellung, KeyCoordinator.newCoordinationData(bestellung.getBestellungId())), best_zub, RequestTimeout.DEFAULT, tx2);
				capi.commitTransaction(tx2);
				logger.debug("Bestellung in Pizzen aufgelöst und in bestellung_zubereitung geschrieben.");
			
			}
		} catch (MzsTimeoutException e) {
			logger.info("Timeout occured.");
		} catch (MzsCoreException | InterruptedException e) {
			e.printStackTrace();
		} catch (NullPointerException e) {
			// Fehler trotz tage- und nächtelanger Suche und etlichen Code-Rewrites nicht zu finden
			// Exception tritt tief in MozartSpace-Code auf und gibt keinen Hinweis auf zugrunde 
			// liegendes Problem
			logger.info("NullPointerException in Transaktion aufgetreten.");
		}
		
	}


	public SpaceUtility getSpace(){
		if (space == null){
			space = new SpaceUtility(filiale);
		}
		return space;
	}
}
